'# Copyright (c) 2008-2011 Wesley Werner
'# Source code distributed under the BSD license

''' <summary>
''' Handles asynchronous stream reading and writing, with events!
''' </summary>
''' <remarks></remarks>
Public Class ASyncCopy

    Private _Abort As Boolean = False
    Private _Running As Boolean = False
    Private _Skipped As Boolean = False


    Public Event CopyStart(ByVal filename As String, ByVal filesize As Long)
    Public Event CopyProgress(ByVal position As Long, ByVal goodRead As Boolean, ByVal retryCount As Integer)
    Public Event CopyDone(ByVal filename As String, ByVal length As Long)
    Public Event CopyCancelled(ByVal filename As String)
    Public Event CopyError(ByVal filename As String, ByVal ex As Exception)
    Public Event CopyMessage(ByVal message As String, ByVal verbosity As Integer)


#Region " cross-thread methods "

    ' Delegates to call Events on the parent form's thread
    Private Delegate Sub CopyStart_Callback(ByVal filename As String, ByVal filesize As Long)
    Private Delegate Sub CopyProgress_Callback(ByVal position As Long, ByVal goodRead As Boolean, ByVal retryCount As Integer)
    Private Delegate Sub CopyDone_Callback(ByVal filename As String, ByVal length As Long)
    Private Delegate Sub CopyCancelled_Callback(ByVal filename As String)
    Private Delegate Sub CopyError_Callback(ByVal filename As String, ByVal ex As Exception)
    Private Delegate Sub CopyMessage_Callback(ByVal message As String, ByVal verbosity As Integer)


    ' these methods will invoke the event on the proper thread to avoid cross-thread exceptions
    Private Sub Call_CopyError(ByVal filename As String, ByVal ex As Exception)

        If Not IsNothing(_parentform) Then
            If _parentform.InvokeRequired Then
                _parentform.Invoke(New CopyError_Callback(AddressOf Call_CopyError), filename, ex)
                Exit Sub
            End If
        End If

        RaiseEvent CopyError(filename, ex)

    End Sub


    ' these methods will invoke the event on the proper thread to avoid cross-thread exceptions
    Private Sub Call_CopyCancelled(ByVal filename As String)

        If Not IsNothing(_parentform) Then
            If _parentform.InvokeRequired Then
                _parentform.Invoke(New CopyCancelled_Callback(AddressOf Call_CopyCancelled), filename)
                Exit Sub
            End If
        End If

        RaiseEvent CopyCancelled(filename)

    End Sub


    ' these methods will invoke the event on the proper thread to avoid cross-thread exceptions
    Private Sub Call_CopyDone(ByVal filename As String, ByVal length As Long)

        If Not IsNothing(_parentform) Then
            If _parentform.InvokeRequired Then
                _parentform.Invoke(New CopyDone_Callback(AddressOf Call_CopyDone), filename, length)
                Exit Sub
            End If
        End If

        RaiseEvent CopyDone(filename, length)

    End Sub


    ' these methods will invoke the event on the proper thread to avoid cross-thread exceptions
    Private Sub Call_CopyProgress(ByVal position As Long, ByVal goodRead As Boolean, ByVal retryCount As Integer)

        If Not IsNothing(_parentform) Then
            If _parentform.InvokeRequired Then
                _parentform.Invoke(New CopyProgress_Callback(AddressOf Call_CopyProgress), position, goodRead, retryCount)
                Exit Sub
            End If
        End If

        RaiseEvent CopyProgress(position, goodRead, retryCount)

    End Sub


    ' these methods will invoke the event on the proper thread to avoid cross-thread exceptions
    Private Sub Call_CopyStart(ByVal filename As String, ByVal filesize As Long)

        If Not IsNothing(_parentform) Then
            If _parentform.InvokeRequired Then
                _parentform.Invoke(New CopyStart_Callback(AddressOf Call_CopyStart), filename, filesize)
                Exit Sub
            End If
        End If

        RaiseEvent CopyStart(filename, filesize)

    End Sub


    ' these methods will invoke the event on the proper thread to avoid cross-thread exceptions
    Private Sub Call_CopyMessage(ByVal message As String, Optional ByVal verbosity As Integer = 1)

        If Not IsNothing(_parentform) Then
            If _parentform.InvokeRequired Then
                _parentform.Invoke(New CopyMessage_Callback(AddressOf Call_CopyMessage), message, verbosity)
                Exit Sub
            End If
        End If

        RaiseEvent CopyMessage(message, verbosity)

    End Sub


#End Region


    ''' <summary>
    ''' Gets whether the copy process is still running
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Running() As Boolean
        Get
            Return _Running
        End Get
    End Property


    Private _Simulation As Boolean
    ''' <summary>
    ''' The copy is a simulation (no writing is done)
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Simulation() As Boolean
        Get
            Return _Simulation
        End Get
        Set(ByVal value As Boolean)
            _Simulation = value
        End Set
    End Property



    Private _parentform As Form
    ''' <summary>
    ''' The form with which to sync the threads when raising events
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property ParentForm() As Form
        Get
            Return _parentform
        End Get
        Set(ByVal value As Form)
            _parentform = value
        End Set
    End Property



    Private _BufferSize As Integer = 1000
    ''' <summary>
    ''' The size in bytes of the read / write buffer
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property BufferSize() As Integer
        Get
            Return _BufferSize
        End Get
        Set(ByVal value As Integer)
            _BufferSize = value
        End Set
    End Property



    Private _JumpSize As Long = 0
    ''' <summary>
    ''' The number of bytes to jump when a block read fails (in addition to the buffer size to skip)
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property JumpSize() As Long
        Get
            Return _JumpSize
        End Get
        Set(ByVal value As Long)
            _JumpSize = value
        End Set
    End Property


    Private _BytesFailed As Long = 0
    ''' <summary>
    ''' The number of reads that failed for this file
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property BytesFailed() As Long
        Get
            Return _BytesFailed
        End Get
    End Property



    Private _BytesSucceeded As Long
    ''' <summary>
    ''' The number of reads that were successful
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property BytesSucceeded() As Long
        Get
            Return _BytesSucceeded
        End Get
    End Property



    Private _RetryCount As Integer = 0
    ''' <summary>
    ''' Get or Set the number of times a block shoud be retried before giving up and moving to the next block
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property RetryCount() As Integer
        Get
            Return _RetryCount
        End Get
        Set(ByVal value As Integer)
            _RetryCount = value
        End Set
    End Property


    ''' <summary>
    ''' Abort the copy process, wait for the CopyCancelled() Event to signal.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Abort()

        _Abort = True
        _Running = False

    End Sub


    ''' <summary>
    ''' Abort the current file copy, but continue with the rest
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub AbortCurrent()

        _Skipped = True
        _Running = False

    End Sub


    ''' <summary>
    ''' public sub to copy a file
    ''' </summary>
    ''' <param name="sourcefile"></param>
    ''' <param name="destfile"></param>
    ''' <remarks></remarks>
    Public Sub CopyFile(ByVal sourcefile As String, ByVal destfile As String)

        _Running = True
        _Skipped = False
        Dim info As New CopyInfo
        info.Sourcefile = sourcefile
        info.DestinationFile = destfile
        System.Threading.ThreadPool.QueueUserWorkItem(New Threading.WaitCallback(AddressOf CopyFile), info)

    End Sub


    ''' <summary>
    ''' internal method to copy async
    ''' </summary>
    ''' <param name="state"></param>
    ''' <remarks></remarks>
    Private Sub CopyFile(ByVal state As Object)

        Dim info As CopyInfo = CType(state, CopyInfo)


        Try

            ' open the file streams 
            Dim srcStream As IO.FileStream = Nothing
            Dim dstStream As IO.FileStream = Nothing
            Dim src As IO.BinaryReader = Nothing
            Dim dst As IO.BinaryWriter = Nothing

            Call_CopyMessage(String.Format("Opening I/O streams for {0}", IO.Path.GetFileName(info.Sourcefile)))

            srcStream = New IO.FileStream(info.Sourcefile, IO.FileMode.Open, IO.FileAccess.Read)

            info.Length = srcStream.Length

            If Not _Simulation Then

                ' ensure the destination path exists
                IO.Directory.CreateDirectory(IO.Path.GetDirectoryName(info.DestinationFile))

                ' setup the output stream
                dstStream = New IO.FileStream(info.DestinationFile, IO.FileMode.Create)

            End If

            src = New IO.BinaryReader(srcStream)
            If Not _Simulation Then dst = New IO.BinaryWriter(dstStream)

            ' fire the copystart event
            Call_CopyStart(info.Sourcefile, srcStream.Length)

            ' set the retry counter
            Dim retry As Integer = 0

            ' define the copy buffer
            Dim buff(_BufferSize - 1) As Byte

            Do

                Try

retry:

                    ' read from the source
                    If src.BaseStream.Position < src.BaseStream.Length Then

                        ' read from the source
                        Call_CopyMessage(String.Format("Reading block {0:x} - {1:x}", src.BaseStream.Position, _BufferSize))
                        buff = src.ReadBytes(_BufferSize)

                        ' update the graph. dont use the async method, its too slow for some reason, and doesnt cause 
                        ' a cross-thread exception.
                        RaiseEvent CopyProgress(src.BaseStream.Position, True, retry)

                        ' increase the success read count
                        _BytesSucceeded += buff.Length

                        ' write to output
                        If Not _Simulation Then dst.Write(buff)

                        ' the buffer read, so reset the retry count
                        retry = 0

                    Else

                        ' end of file
                        _Running = False

                    End If

                Catch ex As Exception

                    ' note: a read error does not advance the base stream position
                    Call_CopyMessage(String.Format("Read error at {0:x}", src.BaseStream.Position))

                    ' update the graph
                    RaiseEvent CopyProgress(src.BaseStream.Position, False, retry)

                    ' increase the retry count
                    retry += 1

                    ' if all our retries are up, move to the next block
                    If (retry > My.Settings.RetryCount) Then

                        _BytesFailed += (_BufferSize + _JumpSize)

                        ' reset the retry count
                        retry = 0

                        ' jump to the next block
                        Call_CopyMessage(String.Format("Skip block {0:x}", src.BaseStream.Position), 2)
                        src.BaseStream.Seek(_BufferSize + _JumpSize, IO.SeekOrigin.Current)

                    Else

                        'Call_CopyMessage(String.Format("Retry block {0:x}", src.BaseStream.Position))

                        ' retry that last read
                        ' yes, this is the first GOTO statement I used in 10 years! honestly. But considering the amount of retries that may happen, I am not risking a stack overflow.
                        GoTo retry

                    End If

                End Try

            Loop Until (Not _Running)


            ' close the streams
            Call_CopyMessage("Flushing and closing I/O file streams.")
            src.Close()

            If Not _Simulation Then
                dst.Flush()
                dst.Close()
            End If

            If (_Skipped Or _Abort) Then
                ' remove skipped files
                IO.File.Delete(info.DestinationFile)
            End If


            ' log that the file is copied
            If (_Abort) Then
                ' notify ui
                Call_CopyCancelled(info.Sourcefile)
            Else
                Call_CopyDone(info.Sourcefile, info.Length)
            End If


        Catch ex As Exception
            Call_CopyError(info.Sourcefile, ex)
        End Try

    End Sub


End Class
